昨天講了知識庫最可怕的失敗:它好好地站在那裡,說著已經不成立的話。
今天講另一種失敗,它一點都不可怕,只是很吵:
整個站變成一片白。
當時的狀況是這樣的:
三個症狀。
而 QA 看到三個症狀時,職業反射是先數數量:這是三個問題,還是一個?
我先說結論——是一個。而且是一行。
在跳進程式碼之前,得先回答一個更基本的問題:壞掉的是環境、是部署,還是程式?
這三種的症狀可以完全一樣,但處理成本差一個數量級。
而且老實說,我發現得不快。因為我是一邊工作一邊做這個網頁的——看到畫面還在載入,我就先去做別的事。等回頭一看,還是沒有資料。
那一刻才知道不妙。
在正式專案裡,遇到「新版上去之後某個東西怪怪的」,我們有一套固定順序,不靠直覺:
我想特別講第 3 步,因為它是很多人不敢做的判斷:有些問題是可以決定不追的。
QA 的職責不是把每一個異常都追到底,是知道哪些異常值得追。把時間花在一個不會重複出現的部署偶發事件上,代價是那些真正會重複出現的問題沒有人看。
這一整套的目的只有一個:在開始查程式碼之前,先讓「我看到的東西」變成一個可信的前提。
否則後面所有的推論,都建立在一個你沒驗證過的假設上——而那正是這個系列從第一天就在講的事。
Uncaught ReferenceError: Cannot access 'PAGES' before initialization
這行字很重要,而且它已經把答案講完了——只要你注意到它不是另一句話。
如果訊息是這樣:
Uncaught ReferenceError: PAGES is not defined
那意思是「根本沒有這個東西」——可能是打錯字、檔案沒載到、變數名不對。
但它說的是 Cannot access 'PAGES' before initialization:東西是有的,只是你太早碰它。
這兩句話指向完全不同的方向。而我看過太多人(包括我自己)看到 ReferenceError 就直接跳到「是不是沒定義」——錯誤訊息的後半句,經常比前半句有用。
程式的形狀大概是這樣:
// 第 153 行
applyLang(); // ← 這裡面會用到 PAGES
// ...中間隔了六百多行...
// 第 836 行
const PAGES = { /* 所有頁面的內容 */ };
applyLang() 在第 153 行就被呼叫,而它需要的 PAGES 到第 836 行才宣告。中間隔了 683 行。
這裡有兩個 JavaScript 的機制在打架:
第一,函式宣告會被提升(hoisting)。 所以 applyLang() 寫在後面、在第 153 行叫得動,完全合法。
第二,const 和 let 也會被提升,但它們在宣告那一行之前處於「暫時性死區」(Temporal Dead Zone, TDZ)。 它們存在,但碰不得——碰了就丟 ReferenceError。
這是 const/let 跟 var 最容易被忽略的差別:
console.log(a); // undefined ← var 會被提升,而且初始化成 undefined
var a = 1;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
const b = 1; // ← const 也被提升了,但在這一行之前是死區
var 給你一個 undefined,讓你帶著錯的值繼續跑;const 直接把你攔下來。
後者其實是好事。 只是這次它攔得有點大。
這才是我真正想講的部分。
一個 <script> 在頂層丟出未被捕捉的錯誤時,整支腳本會停止執行。
所以第 153 行炸掉的那一瞬間,後面的每一行都沒有跑到:
它們不是壞掉了。它們是從來沒有被建立過。
三個症狀、一個原因、一行程式。
如果我當時把它當成三個問題處理,會發生什麼事?
我會開三張卡。三張卡可能被分給不同的人、排進不同的時程,然後各自被「修好」——用三種不同的繞法。而真正那一行,可能到現在還在。
這是 QA 很容易犯、而且代價很高的錯誤,因為它看起來很勤勞。三張卡、三個重現步驟、三份截圖,工作量很飽滿。
我後來把這件事收成一條自己在用的準則:
當多個症狀「同時出現」,優先假設它們共用一個上游。
注意是「同時出現」。如果是陸續出現的,那更可能真的是不同的東西。
但——這裡有一個更重要的補充,而且它是 QA 跟工程師思路的分水嶺:
這個假設必須被驗證,不能只是推論。
驗證方法很簡單:把那一行改掉,三個症狀應該要同時消失。
如果只消失了兩個,那就代表真的還有第二個 bug 躲在後面——而它剛好被第一個 bug 蓋住了,因為腳本根本沒跑到那裡。
我這次是三個一起消失。但我想強調的是:我是驗證過才這樣說的,不是因為我的推論聽起來很合理。
講完這個 bug,我想講另一種壞法——因為我真的碰過,而且它難處理太多了。
那一次的症狀是:Q&A 板整個當掉。 不能留言、不能發問、不能按讚。
而困難的地方在於:
後來查出來是程式衝突。但重點不是原因,是我花了多久才確定它真的壞了。
因為那個畫面沒有說「我壞了」。它說的是「我還在載入」。
而「還在載入」跟「網路有點慢」長得一模一樣。
你的第一反應不會是查 bug,是等一下、重新整理、換個時間再看。你會很自然地把它歸給環境,因為所有你看得到的證據都支持那個解釋。
這種 bug 的成本不在修,在發現。
回頭看,白畫面加上一行紅字,其實是好運氣:它吵、它明確、它主動跑來告訴你哪裡不對。
而這也是為什麼前面那個「先排除環境因素」的步驟必須是一套固定動作,而不是憑感覺——因為當一個東西壞得夠安靜,「是不是網路問題」這個念頭會一直是最舒服、也最容易讓你收手的答案。
我想用這件事收掉 Phase 1,因為它跟後面 25 天要處理的問題,結構完全一樣。
這個 bug 我抓得到,靠的是三件事:
而接下來要面對的那個問題,這三樣一個都沒有。
一個知識庫答錯的時候,不會有 Console。它不會丟 ReferenceError,不會告訴你是第幾行。它會給你一段語氣自信、格式完整、還附了編號的答案。
它壞掉的樣子,比較像那個一直在轉的載入符號——不是紅字。
差別是:載入符號至少讓你覺得哪裡怪怪的。一段寫得很漂亮的錯答案,不會。
而且它也會有「一個原因、多個症狀」這件事——我在 Phase 2 抓到 8 處錯誤,一開始以為是 8 個各自獨立的問題,後來才發現其中好幾處共用同一個根因。
同一套方法,同一種思路,只是那一次沒有 Console 可以看。
那才是真正難的地方。
回顧這五天:
const,三個症狀,和一條「症狀數量不等於缺陷數量」的準則明天開始 Phase 2,也是這整個系列的核心。
我會讓 AI 幫我把 89 張卡統整成 16 張。它會做得非常好——好到我不安。
然後我會做一件 QA 才會做的事:我去驗證它。